package logger

import (
	"errors"
	"net/url"
	"os"
	"path"
	"reflect"
	"regexp"
	"runtime"
	"strings"
	"sync"
	"time"
	"unsafe"

	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

type Config struct {
	//存储路径
	Path string `yaml:"path"`
	//日志记录等级
	Level zapcore.Level `yaml:"level"`
	//是否同步输出到console
	ConsoleOutput bool
}

var (
	regexDateItem       = regexp.MustCompile(`[YmdHis]%`)
	defaultLogName      = "app.log"
	defaultErrorLogName = "error.log"
	windowsSinkOnce     sync.Once
)

// New 默认将错误及以上级别的日志，存入 error.log， 其他级别日志，存入 app.log
func New(config *Config) (*zap.SugaredLogger, error) {
	highPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
		return lvl >= config.Level && lvl >= zapcore.ErrorLevel
	})
	lowPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
		return lvl >= config.Level && lvl < zapcore.ErrorLevel
	})

	consoleDebugging := zapcore.Lock(os.Stdout)
	consoleErrors := zapcore.Lock(os.Stderr)

	cfg := zap.NewProductionEncoderConfig()

	cfg.EncodeTime = zapcore.ISO8601TimeEncoder
	cfg.TimeKey = "time"

	var cores []zapcore.Core
	consoleEncoder := zapcore.NewConsoleEncoder(cfg)
	fileEncoder := zapcore.NewJSONEncoder(cfg)
	if lowWriter, _, err := zap.Open(path.Join(config.Path, defaultLogName)); err != nil {
		return nil, err
	} else {
		cores = append(cores,
			zapcore.NewCore(fileEncoder, lowWriter, lowPriority),
		)
	}
	if highWriter, _, err := zap.Open(path.Join(config.Path, defaultErrorLogName)); err != nil {
		return nil, err
	} else {
		cores = append(cores,
			zapcore.NewCore(fileEncoder, highWriter, highPriority),
		)
	}

	if config.ConsoleOutput {
		cores = append(cores,
			zapcore.NewCore(consoleEncoder, consoleErrors, highPriority),
			zapcore.NewCore(consoleEncoder, consoleDebugging, lowPriority),
		)
	}

	var opts []zap.Option
	if config.Level == zap.DebugLevel {
		opts = append(opts, zap.AddCaller(), zap.Development(), zap.AddStacktrace(zap.DebugLevel))
	} else {
		opts = append(opts, zap.AddStacktrace(zap.ErrorLevel))
	}

	core := zapcore.NewTee(cores...)

	log := zap.New(core, opts...)
	return log.Sugar(), nil
}

// FormatFile 根据 format 格式化文件。
// format 示例：Y%/m%/app_d%.log
func FormatFile(format string) string {
	now := time.Now()

	result := regexDateItem.ReplaceAllStringFunc(format, func(chunk string) string {
		switch chunk {
		case "Y%":
			chunk = now.Format("2006")
		case "m%":
			chunk = now.Format("01")
		case "d%":
			chunk = now.Format("02")
		case "H%":
			chunk = now.Format("15")
		case "i%":
			chunk = now.Format("04")
		case "s%":
			chunk = now.Format("05")
		}
		return chunk
	})
	return result
}

// CompatiblePath 兼容性好的文件路径，主要为解决 windows 下 「couldn't open sink」 的问题
// 如果要处理 windows 下文件日志，建议最好使用本方法处理一下文件路径
func CompatiblePath(osPath string) string {
	if runtime.GOOS != "windows" {
		return osPath
	} else {
		windowsSinkOnce.Do(func() {
			zap.RegisterSink("winfile", newWinFileSink)
		})
		return path.Join("winfile:///", osPath)
	}
}

// 为 winfile schema 做兼容处理
func newWinFileSink(u *url.URL) (zap.Sink, error) {
	// Remove leading slash left by url.Parse()
	var name string
	if u.Path != "" {
		name = u.Path[1:]
	} else if u.Opaque != "" {
		name = u.Opaque[1:]
	} else {
		return nil, errors.New("path error")
	}
	return os.OpenFile(name, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
}

// Struct2Slice 用于将结构体内容保存到日志中，为json的root节点。
// 示例：DefaultLogger.Errorw(tips, Struct2Slice(&detail)...)
func Struct2Slice(detail interface{}) (data []interface{}) {
	e := reflect.ValueOf(detail).Elem()
	for i := 0; i < e.NumField(); i++ {
		data = append(data, strings.ToLower(e.Type().Field(i).Name), getUnexportedField(e.Field(i)))
	}
	return
}

func getUnexportedField(field reflect.Value) interface{} {
	return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Interface()
}
